#![allow(
    clippy::result_unit_err,
    clippy::many_single_char_names,
    clippy::cast_sign_loss,
    clippy::similar_names
)]

const MONTH_DAYS: [u8; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

const MONTH_DAYS_ACC: [u16; 13] = {
    let mut acc = [0; 13];
    let mut i = 2;
    while i < 13 {
        acc[i] = acc[i - 1] + MONTH_DAYS[i - 1] as u16;
        i += 1;
    }
    acc
};

#[must_use]
pub fn is_leap_year(year: u16) -> bool {
    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}

#[must_use]
pub fn validate_ymd(year: u16, month: u8, day: u8) -> bool {
    if month == 0 || month > 12 {
        return false;
    }

    let max_day = if month == 2 && is_leap_year(year) {
        29
    } else {
        MONTH_DAYS[month as usize]
    };

    day > 0 && day <= max_day
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
    year: u16,
    month: u8,
    day: u8,
}

impl Date {
    pub fn parse_ymd(s: &str) -> Result<Self, ()> {
        // yyyy-mm-dd

        if s.len() != 10 {
            return Err(());
        }

        if s.as_bytes()[4] != b'-' || s.as_bytes()[7] != b'-' {
            return Err(());
        }

        let year = s[0..4].parse::<u16>().map_err(|_| ())?;
        let month = s[5..7].parse::<u8>().map_err(|_| ())?;
        let day = s[8..10].parse::<u8>().map_err(|_| ())?;

        if !validate_ymd(year, month, day) {
            return Err(());
        }

        Ok(Self { year, month, day })
    }

    #[must_use]
    pub const fn year(&self) -> u16 {
        self.year
    }

    #[must_use]
    pub const fn month(&self) -> u8 {
        self.month
    }

    #[must_use]
    pub const fn day(&self) -> u8 {
        self.day
    }

    #[must_use]
    pub fn day_of_year(&self) -> u16 {
        let mut acc = MONTH_DAYS_ACC[self.month as usize] + u16::from(self.day);
        if self.month > 2 && is_leap_year(self.year) {
            acc += 1;
        }
        acc
    }

    #[must_use]
    pub fn day_of_week(&self) -> u8 {
        // https://en.wikipedia.org/wiki/Zeller%27s_congruence
        let d = i32::from(self.day);
        let mut m = i32::from(self.month);
        let mut y = i32::from(self.year);
        if m < 3 {
            m += 12;
            y -= 1;
        }
        let k = y % 100;
        let j = y / 100;
        let h = (d + (13 * (m + 1) / 5) + k + (k / 4) + (j / 4) + 5 * j) % 7;
        ((h + 5) % 7 + 1) as u8
    }

    #[must_use]
    pub fn weeks_in_year(&self) -> u8 {
        weeks_in_year(self.year)
    }

    #[must_use]
    pub fn iso_week(&self) -> u8 {
        // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_from_an_ordinal_date
        let y = self.year;
        let doy = self.day_of_year();
        let dow = u16::from(self.day_of_week());
        let w = (10 + doy - dow) / 7;
        if w < 1 {
            weeks_in_year(y - 1)
        } else if w > u16::from(weeks_in_year(y)) {
            1
        } else {
            w as u8
        }
    }

    #[must_use]
    pub fn julian_day_number(&self) -> i32 {
        // https://www.researchgate.net/publication/316558298_Date_Algorithms
        let y = i32::from(self.year - 1);
        let o = i32::from(self.day_of_year());
        o + 365 * y + (y / 4) - (y / 100) + (y / 400) + 1_721_425
    }

    #[must_use]
    pub fn distance(&self, other: &Self) -> u32 {
        let lhs = self.julian_day_number();
        let rhs = other.julian_day_number();
        lhs.abs_diff(rhs)
    }

    #[must_use]
    pub fn end_of_year(&self) -> Self {
        Self {
            year: self.year,
            month: 12,
            day: 31,
        }
    }
}

fn weeks_in_year(year: u16) -> u8 {
    // https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
    let p = |y| (y + y / 4 - y / 100 + y / 400) % 7;
    if p(year) == 4 || p(year - 1) == 3 {
        53
    } else {
        52
    }
}

// #[cfg(test)]
// mod tests {
//     #[test]
//     fn debug() {
//         let time_base = ::time::Date::from_calendar_date(1700, time::Month::January, 1).unwrap();
//         let my_base = super::Date {
//             year: 1700,
//             month: 1,
//             day: 1,
//         };

//         let mut time_date = time_base;

//         for _ in 0..50_0000 {
//             let my_date = super::Date {
//                 year: time_date.year() as u16,
//                 month: time_date.month() as u8,
//                 day: time_date.day(),
//             };

//             assert_eq!(my_date.day_of_year(), time_date.ordinal());

//             assert_eq!(
//                 my_date.day_of_week(),
//                 time_date.weekday().number_from_monday(),
//                 "my_date: {my_date:?}, time_date: {time_date:?}",
//             );

//             assert_eq!(
//                 my_date.weeks_in_year(),
//                 time::util::weeks_in_year(time_date.year())
//             );

//             assert_eq!(my_date.iso_week(), time_date.iso_week());

//             assert_eq!(my_date.julian_day_number(), time_date.to_julian_day());

//             assert_eq!(
//                 my_date.distance(&my_base),
//                 (time_date - time_base).whole_days() as u32
//             );

//             time_date = time_date.next_day().unwrap();
//         }
//     }
// }
